查看原文
其他

模拟经典流体解谜游戏!Cocos Creator 三步实现动态 2D 液体

好巧啊c COCOS 2022-06-10


游戏中可交互的液体是一种颇为吸引人的元素,比如经典人气解谜游戏《鳄鱼小顽皮爱洗澡》就通过简单的「引水入缸」玩法收获了一大票玩家。


在 Cocos Creator 3.x 中,若想实现 2D 液体、同时兼顾运行效率,可以选择使用 Box2D 的物理粒子效果来进行模拟。物理粒子系统除了适合用于模拟液体以外,我们也可以用于模拟任何可变形的物体。这里将解析由引擎技术支持中心带来的 Cocos Creator 的动态 2D 液体解决方案。


点击文末【阅读原文】下载 DEMO:

https://store.cocos.com/app/detail/


PART 1

使用方式

场景搭建


在 Cocos Creator 中新建一个空场景,并创建一个 UICanvas



创建一个用于液体渲染的相机 Camera-001



对于这个相机,使其 ClearFlag = SOLID_COLOR



这个相机的作用是将液体绘制到一张 RT 之后将这个动态纹理投射到 UI 内的某个 sprite 上面。


之后就在场景内布置碰撞体,既然是 Box2D,碰撞体记得选 2D,不然碰撞体就会使用 Bullet 和 Box2d 无关了:



Cocos Creator 里封装了两种物理引擎 Bullet 和 Box2D,两者处于单独的世界。


在 Box2d 里面如果希望碰撞体之间的碰撞有效,那么至少有一方需要持有 Rigidbody2D 组件。因此需要给碰撞体添加一个 RigidBody2D,其类型选择为 static 。这样物理引擎不会去模拟他的速度和受力情况。



添加液体


新建一个空的 UINode,也就是只有 UITransform 组件的一个 Node:



为其添加 WaterRender 这个组件:



之后指定好他的一些值:


  • 自定义材质;


  • FixError :FixError 指向的是一张1个 2x2 的纯色小纹理;


  • 水管:水管由多个碰撞体构成,这样可以约束液体,使其往我们想要的地方流动;


效果预览


播放预览一下效果(这里开启了物理调试,可以更清晰的观察到粒子的运行状态):



PART 2

前置知识

物理引擎


Box2D 是一款轻量级的 2D 游戏物理引擎。主流游戏引擎的 2D 物理部分大都使用 Box2D 来完成的。在物理引擎模拟中,通过质心的受力,计算出其速度和加速度等最终得到物体所在的位置,之后渲染引擎会读取物理引擎的计算结果并将其应用到渲染上。


LiquidFun


LiquidFun 是基于 Box2D 的扩展库,作用就是给 Box2D 添加了模拟液体的粒子系统。该库由谷歌高级程序员 Kentaro Suto 开发,源代码由 C++ 编写,并翻译为 JavaScript。


组装器


在游戏引擎中,绘制精灵或者模型时,都需要通过生成特定的顶点,并调用驱动方法(OpenGL,DirectX ...等)绘制到屏幕上。在 Cocos Creator 里面,如果我们要绘制一系列顶点到屏幕上,需要使用到 Assembler 组装器


组装器顾名思义就是将顶点组装起来,以供渲染组件使用。通过这个 Assembler,可以自定义顶点的位置、颜色、纹理坐标、索引。


Cocos Creator 里有多种 Assembler

/**
 * simple 组装器
 * 可通过 `UI.simple` 获取该组装器。
 */+
export const simple: IAssembler 

/**
 Tiled组装器
*/
export const tiled: IAssembler = 

...


DEMO 中通过读取物理引擎内粒子的位置,计算出了顶点缓存内所有顶点的相关信息。


PART 3

原理解析

render.ts 里面有两个类 WaterRenderWaterAssembler


WaterRender 解析


WaterRender 是整个 DEMO 的核心类,负责粒子的创建和渲染。


Renderable2D


WaterRender 继承自 Renderable2D在 Cocos Creator 中,任何需要渲染的 Node 对象都会持有一个 RenderableComponent,其中 Renderable2D 是 Cocos Creator 中渲染 2D 组件的基类。


通过重写 _render 方法,自定义自己的渲染方案。这里通过使用自定义的 _assembler 来组装需要绘制的几何体。

/**
*commitComp会提交当前的渲染数据给渲染管线
*/

protected _render(render: any) {
    render.commitComp(thisthis.fixError, this._assembler!, null);
}


创建粒子系统


我们可以把液体理解为由很多个小的水滴组成。这样对于物理引擎来说,就可以选择使用粒子系统,以一种高效的方式,来模拟大量水滴运动的行为。


创建粒子系统:

  var psd_def = {
            strictContactCheck: false,
            density: 1.0,
            gravityScale: 1.0,
            radius: 0.35,  //这里指定了粒子的半径
         
            ...
   }

this._particles = this._world.physicsWorld.impl.CreateParticleSystem(psd);


创建粒子组:

var particleGroupDef = {
   ...
    shape: null,
    position: {
        x: this.particleBox.node.getWorldPosition().x / PHYSICS_2D_PTM_RATIO,
        y: this.particleBox.node.getWorldPosition().y / PHYSICS_2D_PTM_RATIO
    },
    // @ts-ignore
    shape: this.particleBox._shape._createShapes(1.01.0)[0]
};

this._particleGroup = this._particles.CreateParticleGroup(particleGroupDef);
this.SetParticles(this._particles);


粒子组为粒子发射器定义了一组粒子,这些粒子拥有自定义的形状:

//创建BoxCollider2D的几何形状
shape: this.particleBox._shape._createShapes(1.01.0)[0]


通过对液体的观察,可以发现液体有一些常见的特性:

  • 水往低处流,水滴会沿着碰撞体的表面进行移动 gravityScale: 1.0,定义了粒子受重力影响的系数;

  • 黏连性,可观察到两个水滴靠近时,会在液体的作用力下相互吸引,通过定义 viscousStrength 来定义粒子的黏连;

  • 压缩,液体粒子间会进行压缩,由下面的值来定义粒子允许进行的压缩:

  pressureStrength 
  staticPressureStrength
  staticPressureRelaxation 
  staticPressureIterations
  • 表面张力, 我们都知道在水面上放硬币,硬币不会沉底的实验。这个就是液体的表面张力。通过下面两个属性,可以调整液体的表面张力:

    surfaceTensionPressureStrength: 0.2,
    surfaceTensionNormalStrength: 0.2,


WaterAssembler 解析


WaterAssemblerRenderableComponent 提供顶点缓存的定制。


在这个类里面,通过访问粒子系统的每一个粒子的位置,生成4个单独的顶点:

let posBuff = particles.GetPositionBuffer();
let r = particles.GetRadius() * PHYSICS_2D_PTM_RATIO * 3;
 
for (let i = 0; i < particleCount; ++i) {
    let x = posBuff[i].x * PHYSICS_2D_PTM_RATIO;
    let y = posBuff[i].y * PHYSICS_2D_PTM_RATIO;

    // left-bottom
    vbuf[vertexOffset++] = x - r; //x 
    vbuf[vertexOffset++] = y - r; //y
    vbuf[vertexOffset++] = 0// z 
    vbuf[vertexOffset++] = x; // u
    vbuf[vertexOffset++] = y; // v
   ...
}


最后计算索引缓存:

// fill indices
const ibuf = buffer.iData!;
for (let i = 0; i < particleCount; ++i) {
    ibuf[indicesOffset++] = vertexId;
    ibuf[indicesOffset++] = vertexId + 1;
    ibuf[indicesOffset++] = vertexId + 2;
    ibuf[indicesOffset++] = vertexId + 1;
    ibuf[indicesOffset++] = vertexId + 3;
    ibuf[indicesOffset++] = vertexId + 2;
    vertexId += 4;
}


顶点缓存描述了顶点的数据,索引缓存指定了顶点的绘制顺序。


这样就生成了一个基于粒子中心点的矩形。但是我们最终看到的是圆形,这里的魔法就是通过材质和 Effect 系统来解决的。


材质和 Shader 解析



模拟时,需要使用 effect.effect 特效俩来模拟。


注意这里选择的是 transparent 的 technique:



effect.effectvert 函数内,计算了两个传输到 frag 的变量:v_cornerv_center ,这两个变量代表的是粒子位置的中心点和角落的位置:

  out vec2 v_corner;
  out vec2 v_center;

  vec4 vert () {
    vec4 pos = vec4(a_position.xy, 01);    

    // no a_corner in web version
    // use a_position instead of a_corner
    v_corner = a_position.xy * reverseRes;
    // 由于粒子是纯色的,texCoord 里面记录的是粒子的中心点位置
    v_center = a_texCoord.xy * reverseRes;

    v_corner.y *= yratio;
    v_center.y *= yratio;

    return cc_matViewProj * pos;
  }


这两个变量在 frag 里面通过 smoothstep 进行插值的计算:

smoothstep(edge0, edge1, x) 
这个函数会根据x计算赫尔米特插值:
t = clamp((x - edge0) / (edge1 - edge0), 0.01.0);
return t * t * (3.0 - 2.0 * t);



在 frag() 函数内通过计算像素位置和粒子中心的距离,使用 smoothstep 进行插值,粒子的半径就会被控制在3倍到1倍半径之间。同时由于是根据中心和半径计算,粒子也会从矩形变成圆形:

  in vec2 v_corner;
  in vec2 v_center;

  vec4 frag () {
    float mask = smoothstep(radius * 3., radius, distance(v_corner, v_center));
    return vec4(1.01.01.0, mask);
  }


此时绘制出来的粒子颜色是白色:



最后通过 display.effect 配合 render texture 将其渲染为蓝色:



display.effect 使用了属性查看器内传入的颜色 color

  in vec4 color;

  #if USE_TEXTURE
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
  #endif

  vec4 frag () {
    vec4 o = vec4(1111);

    #if USE_TEXTURE
      o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
      #if IS_GRAY
        float gray  = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
        o.r = o.g = o.b = gray;
      #endif
    #endif

    o.a = smoothstep(0.951.0, o.a);
    o *= color;

    ALPHA_TEST(o);
    return o;
  }
  


这个时候由于 alpha 的问题会出现一些毛边:



因此通过 smoothstep(0.95, 1.0, o.a) ,将像素的 alpha 值都控制在0.95到1之间。


通过这个渲染我们可以看到,其实做游戏不一定非要真实的去模拟,我们只要骗过眼睛就能做出很好的效果了!




本期分享就到这里DEMO 地址见评论。在之前我们也和大家分享过 Cocos Creator 3.x 的 2D 动态光照2D 实时阴影的技术实现方案,更多方案与 DEMO 请移步论坛集中贴,如果有想了解的技术或效果实现,欢迎在评论区留言,后续我们会更新更多关于游戏引擎的技术分享。


论坛集中贴

https://forum.cocos.org/t/topic/124637


参考链接&扩展阅读

Box2D 开源:

https://github.com/erincatto/box2d

LiquidFun 官网:

https://github.com/google/liquidfun

LiquidFun 参考文档:

https://google.github.io/liquidfun/Programmers-Guide/html/index.html

SmoothStep:

https://en.wikipedia.org/wiki/Smoothstep


往期精彩

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存